﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Cache;
using System.Text;
using System.Threading;
using System.Xml;
using System.Globalization;

namespace UPNP.IO
{
    internal abstract class DeviceServiceQuery
    {
        public static readonly Encoding SoapEncoding = new UTF8Encoding(false);
        public static readonly CultureInfo SoapCulture = CultureInfo.CreateSpecificCulture("en-US");
        public static readonly string SoapNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
        public static readonly string SoapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/";

        protected readonly log4net.ILog _log;
        private readonly HttpWebRequest _request;
        private readonly ManualResetEvent _event;
        private volatile Exception _error;
        private volatile object _result;
        private volatile object[] _arguments;

        protected object[] Arguments
        {
            get { return _arguments; }
        }

        public object DoQuery()
        {
            return DoQuery(new object[0]);
        }

        public object DoQuery(object[] args)
        {
            // prepare
            _error = null;
            _result = null;
            _arguments = args;

            // start blocking
            _event.Reset();

            // start request and wait
            BeginRequest();

            _event.WaitOne();

            // handle any error
            if (_error != null)
                throw new Exception("Failed to query service; see Inner exception for details", _error);

            // finalize
            return _result;
        }

        protected abstract void PrepareRequest(HttpWebRequest req);
        protected abstract void WriteRequest(XmlTextWriter xw);
        protected abstract object HandleResponse(HttpWebResponse response, XmlDocument body);

        #region error handling

        protected virtual void HandleHttpError(HttpStatusCode error)
        {
            Contract.Requires(error > 0);
            Contract.Requires(error != HttpStatusCode.OK);

            var errorCode = (int)error;
            var errorDesc = error.ToString();

            switch (error)
            {
                case HttpStatusCode.MethodNotAllowed:
                    throw new InvalidOperationException(string.Format(
                        "method now allowed; use M-POST instead of POST: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case HttpStatusCode.InternalServerError:
                    // this is fine since the message
                    // will contain a fault
                    return;

                default:
                    throw new DeviceCommunicationException(string.Format(
                        "HTTP communication error occurred: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));
            }
        }

        protected virtual void HandleSoapFault(XmlElement fault)
        {
            Contract.Requires(fault != null);

            // is upnp error?
            var upnpFault = (XmlElement)fault.SelectSingleNode("./*[local-name()='detail']/*[local-name()='UPnPError']");

            if (upnpFault == null)
            {
                throw new DeviceCommunicationException("unknown SOAP fault received: " + fault.InnerXml);
            }

            // handle upnp fault
            var errorCode = Convert.ToInt32(upnpFault.SelectSingleNodeInnerTextByLocalName("errorCode", false));
            var errorDesc = upnpFault.SelectSingleNodeInnerTextByLocalName("errorDescription", true);

            switch (errorCode)
            {
                case 401 /*Invalid Action*/:
                case 404 /*Invalid Var*/:
                case 602 /*Optional Action Not Implemented*/:
                    throw new NotImplementedException(string.Format(
                        "invalid action/variable: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 501 /*Action Failed*/:
                    throw new InvalidOperationException(string.Format(
                        "action failed: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 606 /*Action not authorized*/:
                    throw new InvalidOperationException(string.Format(
                        "not authorized: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 612 /*No such session*/:
                    throw new InvalidOperationException(string.Format(
                        "session key is invalid/expired: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 600 /*Argument Value Invalid*/:
                    throw new ArgumentException(string.Format(
                        "invalid argument: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 605 /*String Argument Too Long*/:
                    throw new ArgumentException(string.Format(
                        "string argument too long: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 601 /*Argument Value Out of Range*/:
                    throw new ArgumentOutOfRangeException(string.Format(
                        "argument value out of range: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 603 /*Out of Memory*/:
                    throw new DeviceCommunicationException(string.Format(
                        "out of memory on device: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 607 /*Signature failure*/:
                    throw new DeviceCommunicationException(string.Format(
                        "signature is invalid: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 608 /*Signature missing*/:
                    throw new DeviceCommunicationException(string.Format(
                        "signature is missing: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 609 /*Not encrypted*/:
                    throw new DeviceCommunicationException(string.Format(
                        "request is not encrypted: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 610 /*Invalid sequence*/:
                    throw new DeviceCommunicationException(string.Format(
                        "provided <sequence/> is invalid: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 611 /*Invalid control URL*/:
                    throw new DeviceCommunicationException(string.Format(
                        "<freshness/> or HTTP header control URL invalid: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                case 604 /*Human Intervention Required*/:
                    throw new InvalidOperationException(string.Format(
                        "human intervention required: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));

                default:
                    throw new DeviceCommunicationException(string.Format(
                        "unknown UPNP fault received: {1} ({0})",
                        errorCode,
                        errorDesc
                        ));
            }
        }

        #endregion

        private void BeginRequest()
        {
            try
            {
                // prepare
                PrepareRequest(_request);

                // create request data
                byte[] requestData;

                using (var ms = new MemoryStream())
                {
                    using (var xw = new XmlTextWriter(ms, SoapEncoding))
                    {
                        xw.Formatting = Formatting.Indented;

                        WriteRequest(xw);
                    }

                    requestData = ms.ToArray();
                }

                // start request
                _request.ContentLength = requestData.LongLength;
                _request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
                _request.UserAgent = "upnp.codeplex.com (UPnP/1.0)";

                _request.BeginGetRequestStream(
                    o => EndRequestAndBeginResponse(o),
                    requestData
                    );
            }
            catch (Exception x)
            {
                _log.Error("Failed to start request", x);

                _error = x;
            }
            finally
            {
                // unblock execution
                _event.Set();
            }
        }

        private void EndRequestAndBeginResponse(IAsyncResult a)
        {
            try
            {
                var requestData = (byte[])a.AsyncState;

                // debug
                if (_log.IsDebugEnabled)
                {
                    _log.Debug("REQUEST: " + SoapEncoding.GetString(requestData));
                }

                // push request body
                using (var s = _request.EndGetRequestStream(a))
                {
                    s.Write(requestData, 0, requestData.Length);
                }

                // listen for response
                _request.BeginGetResponse(
                    o => EndResponse(o),
                    null
                    );
            }
            catch (Exception x)
            {
                _log.Error("Failed to send request", x);

                _error = x;
            }
            finally
            {
                // unblock execution
                _event.Set();
            }
        }

        private void EndResponse(IAsyncResult a)
        {
            XmlDocument xml = null;

            try
            {
                // gather response
                HttpWebResponse response;
                WebException error = null;

                try
                {
                    response = (HttpWebResponse)_request.EndGetResponse(a);
                }
                catch (WebException x)
                {
                    response = (HttpWebResponse)x.Response;
                    error = x;
                }

                // parse response
                using (response)
                {
                    // check for http error
                    if (error != null)
                    {
                        if (error.Status == WebExceptionStatus.ProtocolError)
                        {
                            HandleHttpError(response.StatusCode);
                        }
                        else
                        {
                            throw new DeviceCommunicationException("HTTP communication error: " + error.Status, error);
                        }
                    }

                    // load body
                    xml = new XmlDocument();

                    xml.Load(response.GetResponseStream());

                    // check for error
                    var errElem = (XmlElement)xml.SelectSingleNode(
                        "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='Fault']"
                        );

                    if (errElem != null)
                    {
                        HandleSoapFault(errElem);
                    }

                    // handle reponse
                    _result = HandleResponse(response, xml);
                }
            }
            catch (Exception x)
            {
                _log.Error("Failed to parse response", x);

                if ((xml != null) && _log.IsDebugEnabled)
                {
                    _log.Debug("  RESPONSE: " + xml.OuterXml);
                }

                _error = x;
            }
            finally
            {
                // unblock execution
                _event.Set();
            }
        }

        public DeviceServiceQuery(DeviceDom.Service service, log4net.ILog log = null)
        {
            ////Contract.Requires(log != null);
            Contract.Requires(service != null);

            _log = log ?? log4net.LogManager.GetLogger(GetType());

            _event = new ManualResetEvent(true);
            _request = (HttpWebRequest)WebRequest.Create(service.ControlUrl);
        }
    }
}
